﻿using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;

namespace Sonce.Editor
{
	/// <summary>
	/// This class handles panning and zooming the content.
	/// </summary>
	/// <remarks>
	/// References:
	/// http://stackoverflow.com/questions/741956/wpf-pan-zoom-image
	/// http://blogs.windowsclient.net/joeyw/archive/2008/08/05/pan-and-zoom-deepzoom-style-in-wpf.aspx
	/// </remarks>
	public partial class ViewHelper
	{
		#region Members
		protected Point start;
		protected Point origin;
		protected FrameworkElement container;
		protected List<ViewHelper> syncList;
		#endregion

		#region Constructors
		/// <summary>
		/// Initializes a new ViewHelper instance with default parameters.
		/// </summary>
		public ViewHelper()
		{
			this.CanAnimate = true;
		}

		/// <summary>
		/// Initializes a new ViewHelper instance with default parameters and sets a content container.
		/// </summary>
		/// <param name="container"></param>
		public ViewHelper(Panel container)
			: this()
		{
			this.Container = container;
		}
		#endregion

		#region Properties
		/// <summary>
		/// Gets or sets a content container. This panel should be empty, content is dyamically loaded.
		/// Panel's RenderTransform property is overridden if exists.
		/// </summary>
		public FrameworkElement Container
		{
			get
			{
				return this.container;
			}
			set
			{
				this.container = value;
				TransformGroup group = new TransformGroup();
				group.Children.Add(new TranslateTransform()); //Must be on the first place (find PropertyPath in the code, tt is placed on [0] position
				group.Children.Add(new ScaleTransform());
				this.container.RenderTransform = group;
			}
		}

		/// <summary>
		/// Gets or sets a value indicating whether panning or zooming transitions should be animated.
		/// </summary>
		public bool CanAnimate
		{
			get;
			set;
		}

		/// <summary>
		/// Gets or sets an absolute zoom value. Value 1.0 for original size, greater than 1.0 for zoom in,
		/// lower than 1.0 for zoom out, negative to flip by both coordinates.
		/// </summary>
		public double CurrentZoomValue
		{
			get
			{
				return st.ScaleX;
			}
			set
			{
				ZoomTo(value);
			}
		}

		/// <summary>
		/// Gets or sets an absolute pan value. Coordinate (0,0) is upper left corner.
		/// </summary>
		public Point CurrentPanValue
		{
			get
			{
				return new Point(tt.X, tt.Y);
			}
			set
			{
				PanTo(value);
			}
		}

		protected TranslateTransform tt
		{
			get
			{
				return (TranslateTransform)(((TransformGroup)this.container.RenderTransform).Children[0]);
			}
		}

		protected ScaleTransform st
		{
			get
			{
				return (ScaleTransform)(((TransformGroup)this.container.RenderTransform).Children[1]);
			}
		}
		#endregion

		#region Pan and zoom
		/// <summary>
		/// Overloaded. Pan to an absolute coordinate without animation.
		/// </summary>
		/// <param name="offset">Absolute coordinate. Coordinate (0,0) is upper left corner.</param>
		public void PanTo(Point offset)
		{
			PanTo(offset, 0);
		}

		/// <summary>
		/// Pan to an absolute coordinate with animation.
		/// </summary>
		/// <param name="offset">Absolute coordinate. Coordinate (0,0) is upper left corner.</param>
		/// <param name="duration">Animation duration in milliseconds.</param>
		public void PanTo(Point offset, double duration)
		{
			//Smooth TranslateTransform
			DoubleAnimation animX = CreateAnimation(offset.X, duration);
			DoubleAnimation animY = CreateAnimation(offset.Y, duration);
			Storyboard storyboard = new Storyboard();
			storyboard.Children.Add(animX);
			storyboard.Children.Add(animY);
			Storyboard.SetTarget(animX, this.container);
			Storyboard.SetTarget(animY, this.container);
			Storyboard.SetTargetProperty(animX, new PropertyPath("(UIElement.RenderTransform).(TransformGroup.Children)[0].(TranslateTransform.X)"));
			Storyboard.SetTargetProperty(animY, new PropertyPath("(UIElement.RenderTransform).(TransformGroup.Children)[0].(TranslateTransform.Y)"));
			storyboard.Begin();

			//WPF version
			//tt.BeginAnimation(TranslateTransform.XProperty, CreateAnimation(offset.X, duration));
			//tt.BeginAnimation(TranslateTransform.YProperty, CreateAnimation(offset.Y, duration));
		}

		/// <summary>
		/// Overloaded. Pan by a relative offset without animation.
		/// </summary>
		/// <param name="offset">Relative offset.</param>
		public void PanBy(Point offset)
		{
			PanBy(offset, 0);
		}

		/// <summary>
		/// Pan by a relative offset with animation.
		/// </summary>
		/// <param name="offset">Relative offset.</param>
		/// <param name="duration">Animation duration in milliseconds.</param>
		public void PanBy(Point offset, double duration)
		{
			Point point = new Point(tt.X + offset.X, tt.Y + offset.Y);
			PanTo(point, duration);
		}

		/// <summary>
		/// Overloaded. Zoom to an absolute scale without animation.
		/// </summary>
		/// <param name="scale">Scale value (1.0 for original size).</param>
		public void ZoomTo(double scale)
		{
			ZoomTo(scale, 40);
		}

		/// <summary>
		/// Zoom to an absolute scale with animation.
		/// </summary>
		/// <param name="scale">Scale value (1.0 for original size).</param>
		/// <param name="duration">Animation duration in milliseconds.</param>
		public void ZoomTo(double scale, double duration)
		{
			//Smooth ScaleTransform
			DoubleAnimation animScaleX = CreateAnimation(scale, duration);
			DoubleAnimation animScaleY = CreateAnimation(scale, duration);
			Storyboard storyboard = new Storyboard();
			storyboard.Children.Add(animScaleX);
			storyboard.Children.Add(animScaleY);
			Storyboard.SetTarget(animScaleX, this.container);
			Storyboard.SetTarget(animScaleY, this.container);
			Storyboard.SetTargetProperty(animScaleX, new PropertyPath("(UIElement.RenderTransform).(TransformGroup.Children)[1].(ScaleTransform.ScaleX)"));
			Storyboard.SetTargetProperty(animScaleY, new PropertyPath("(UIElement.RenderTransform).(TransformGroup.Children)[1].(ScaleTransform.ScaleY)"));
			storyboard.Begin();

			//WPF version
			//st.BeginAnimation(ScaleTransform.ScaleXProperty, CreateAnimation(scale, duration));
			//st.BeginAnimation(ScaleTransform.ScaleYProperty, CreateAnimation(scale, duration));
		}

		/// <summary>
		/// Overloaded. Zoom by amount without animation.
		/// </summary>
		/// <param name="scale">Amount of change in scale. Positive to zoom in, negative to zoom out.</param>
		public void ZoomBy(double delta)
		{
			ZoomBy(delta, 0);
		}

		/// <summary>
		/// Zoom by amount with animation.
		/// </summary>
		/// <param name="scale">Amount of change in scale. Positive to zoom in, negative to zoom out.</param>
		/// <param name="duration">Animation duration in milliseconds.</param>
		public void ZoomBy(double delta, double duration)
		{
			ZoomTo(this.CurrentZoomValue + delta, duration);
		}

		/// <summary>
		/// Overloaded. Zoom content to the original size and pan to the origin.
		/// </summary>
		public void ZoomOriginal()
		{
			ZoomOriginal(0);
		}

		/// <summary>
		/// Zoom content to the original size and pan to the origin.
		/// </summary>
		/// <param name="duration">Animation duration in milliseconds.</param>
		public void ZoomOriginal(double duration)
		{
			PanTo(new Point(0, 0), duration);
			ZoomTo(1.0, duration);
		}

		/// <summary>
		/// Overloaded. Zoom content to fit into the container.
		/// </summary>
		public void ZoomFit()
		{
			ZoomFit(0);
		}

		/// <summary>
		/// Zoom content to fit into the container.
		/// </summary>
		/// <param name="duration">Animation duration in milliseconds.</param>
		public void ZoomFit(double duration)
		{
			//if (this.container.Children.Count > 0)
			//{
			//    FrameworkElement element = this.container.Children[0] as FrameworkElement;
			//    double scaleX = this.container.ActualWidth / element.ActualWidth;
			//    double scaleY = this.container.ActualHeight / element.ActualHeight;

			//    if (scaleX < scaleY)
			//    {
			//        double offsetY = Math.Abs(this.container.ActualHeight - scaleX * element.ActualHeight) / 2;
			//        PanTo(new Point(0, offsetY), duration);
			//        ZoomTo(scaleX, duration);
			//    }
			//    else
			//    {
			//        double offsetX = Math.Abs(this.container.ActualWidth - scaleY * element.ActualWidth) / 2;
			//        PanTo(new Point(offsetX, 0), duration);
			//        ZoomTo(scaleY, duration);
			//    }
			//}
		}

		/// <summary>
		/// Zoom content to fit into the container.
		/// </summary>
		/// <param name="duration">Animation duration in milliseconds.</param>
		public void ZoomFitElement(double duration, FrameworkElement container)
		{
			if (this.container is Panel && ((Panel)this.container).Children.Count > 0)
			{
				FrameworkElement element = ((Panel)this.container).Children[0] as FrameworkElement;
				double scaleX = container.ActualWidth / this.container.ActualWidth;
				double scaleY = container.ActualHeight / this.container.ActualHeight;
				double scale = 0;
				double offset = 0;

				if (scaleX < scaleY)
				{
					scale = scaleX;
					offset = scale * element.ActualHeight / 4;
					PanTo(new Point(0, offset), duration);
				}
				else
				{
					scale = scaleY;
					offset = scale * element.ActualWidth / 4;
					PanTo(new Point(offset, 0), duration);
				}

				ZoomTo(scale, duration);
			}
		}

		/// <summary>
		/// Zoom in or out of the content.
		/// </summary>
		/// <param name="zoomFactor">Factor to mutliply the zoom level by (greater than 1.0 for scale down, lower than 1.0 for scale up, negative to invert).</param>
		/// <param name="imageMousePosition">Logical mouse position relative to the original content.</param>
		/// <param name="borderMousePosition">Actual mouse position on the screen (relative to the parent window)</param>
		/// <remarks>
		/// Reference: http://blogs.windowsclient.net/joeyw/archive/2008/08/05/pan-and-zoom-deepzoom-style-in-wpf.aspx
		/// </remarks>
		public void Zoom(double zoomFactor, Point imageMousePosition, Point borderMousePosition)
		{
			const double duration = 40; //Milliseconds
			double targetZoom = st.ScaleX * zoomFactor;
			double targetX = -1 * (imageMousePosition.X * targetZoom - borderMousePosition.X);
			double targetY = -1 * (imageMousePosition.Y * targetZoom - borderMousePosition.Y);

			//Smooth TranslateTransform
			DoubleAnimation animX = CreateAnimation(targetX, duration);
			DoubleAnimation animY = CreateAnimation(targetY, duration);
			Storyboard sbTranslate = new Storyboard();
			sbTranslate.Children.Add(animX);
			sbTranslate.Children.Add(animY);
			Storyboard.SetTarget(animX, this.container);
			Storyboard.SetTarget(animY, this.container);
			Storyboard.SetTargetProperty(animX, new PropertyPath("(UIElement.RenderTransform).(TransformGroup.Children)[0].(TranslateTransform.X)"));
			Storyboard.SetTargetProperty(animY, new PropertyPath("(UIElement.RenderTransform).(TransformGroup.Children)[0].(TranslateTransform.Y)"));
			sbTranslate.Begin();

			//Smooth ScaleTransform
			DoubleAnimation animScaleX = CreateAnimation(targetZoom, duration);
			DoubleAnimation animScaleY = CreateAnimation(targetZoom, duration);
			Storyboard sbScale = new Storyboard();
			sbScale.Children.Add(animScaleX);
			sbScale.Children.Add(animScaleY);
			Storyboard.SetTarget(animScaleX, this.container);
			Storyboard.SetTarget(animScaleY, this.container);
			Storyboard.SetTargetProperty(animScaleX, new PropertyPath("(UIElement.RenderTransform).(TransformGroup.Children)[1].(ScaleTransform.X)"));
			Storyboard.SetTargetProperty(animScaleY, new PropertyPath("(UIElement.RenderTransform).(TransformGroup.Children)[1].(ScaleTransform.Y)"));
			sbScale.Begin();
		}
		#endregion

		#region Protected methods
		/// <summary>
		/// Create a DoubleAnimation instance.
		/// </summary>
		/// <param name="amount">Target value; 'To' property.</param>
		/// <param name="duration">Animation duration in milliseconds.</param>
		/// <returns>Returns a created DoubleAnimation instance.</returns>
		protected DoubleAnimation CreateAnimation(double amount, double duration)
		{
			DoubleAnimation anim = new DoubleAnimation();
			duration = (this.CanAnimate ? duration : 0);
			anim.Duration = TimeSpan.FromMilliseconds(duration);
			anim.To = (double.IsInfinity(amount) || double.IsNaN(amount) ? null : (double?)amount);
			return anim;
		}
		#endregion

		#region Bubble events
		public event EventHandler<EventArgs<FrameworkElement>> Loaded;
		protected virtual void OnLoaded(FrameworkElement element)
		{
			if (Loaded != null)
			{
				Loaded(this, new EventArgs<FrameworkElement>(element));
			}
		}
		#endregion
	}
}
